﻿using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using Mongoose.Windows;

namespace Mongoose.Windows.Controls
{
    internal enum PadLayers
    {
        UnselectedPadLayer = 0,
        SelectedPadLayer = 1,
        UnselectedPinnedPadLayer = 2,
        SelectedPinnedPadLayer = 3
    }

    /// <summary>
    /// Pad object usable inside a Surface
    /// </summary>
    public class Pad : ListBoxItem
    {
        /// <summary>
        /// Initializes the <see cref="Pad"/> class.
        /// </summary>
        static Pad()
        {
            DefaultStyleKeyProperty.OverrideMetadata(typeof(Pad), new FrameworkPropertyMetadata(typeof(Pad)));
            Pad.StyleProperty.OverrideMetadata(typeof(Pad), new FrameworkPropertyMetadata(DefaultStyle));
            Pad.TemplateProperty.OverrideMetadata(typeof(Pad), new FrameworkPropertyMetadata(DefaultTemplate));
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="Pad"/> class.
        /// </summary>
        /// <exception cref="System.ArgumentNullException">container</exception>
        public Pad()
        {
            // Attach container on load
            Loaded += OnLoaded;
            RenderTransform = MatrixTransform;
            CacheMode = new BitmapCache() { RenderAtScale = 2.0D, EnableClearType = true, SnapsToDevicePixels = true };
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="Pad"/> class.
        /// </summary>
        /// <param name="container">The pad container.</param>
        /// <exception cref="System.ArgumentNullException">container</exception>
        public Pad(IPadContainer container)
        {
            if (container == null)
            {
                throw new ArgumentNullException("container");
            }
            this.container = container;
            RenderTransform = MatrixTransform;
            CacheMode = new BitmapCache() { RenderAtScale = 2.0D, EnableClearType = true, SnapsToDevicePixels = true };
        }

        public string DisplayName
        {
            get { return (string)GetValue(DisplayNameProperty); }
            set { SetValue(DisplayNameProperty, value); }
        }

        // Using a DependencyProperty as the backing store for DisplayName.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty DisplayNameProperty =
            DependencyProperty.Register("DisplayName", typeof(string), typeof(Pad), new PropertyMetadata(string.Empty));

        public Point Position
        {
            get { return (Point)GetValue(PositionProperty); }
            set { SetValue(PositionProperty, value); }
        }

        // Using a DependencyProperty as the backing store for Position.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty PositionProperty =
            DependencyProperty.Register("Position", typeof(Point), typeof(Pad), new PropertyMetadata(default(Point), OnPositionChanged));

        public Matrix SurfaceMatrixTransform
        {
            get { return (Matrix)GetValue(SurfaceMatrixTransformProperty); }
            set { SetValue(SurfaceMatrixTransformProperty, value); }
        }

        // Using a DependencyProperty as the backing store for SurfaceMatrixTransform.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty SurfaceMatrixTransformProperty =
            DependencyProperty.Register("SurfaceMatrixTransform", typeof(Matrix), typeof(Pad), new PropertyMetadata(Matrix.Identity, OnSurfaceMatrixTransformChanged));

        private static void OnSurfaceMatrixTransformChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
        {
            var pad = sender as Pad;
            if (pad == null)
            {
                throw new InvalidOperationException();
            }

            if (!pad.IsPinned)
            {
                pad.UpdateRenderTransform();
            }
        }

        /// <summary>
        /// Gets a value indicating whether this instance is pinned.
        /// </summary>
        /// <value>
        ///   <c>true</c> if this instance is pinned; otherwise, <c>false</c>.
        /// </value>
        public bool IsPinned 
        {
            get { return isPinned; }
        }

        /// <summary>
        /// Switches the pin mode.
        /// </summary>
        public void SwitchPinMode()
        {
            isPinned = !isPinned;

            if (IsPinned)
            {
                var matrixTransform = MatrixTransform.Matrix;
                matrixTransform.Scale(1 / MatrixTransform.Matrix.M11, 1 / MatrixTransform.Matrix.M22);
                matrixTransform.OffsetX *= MatrixTransform.Matrix.M11;
                matrixTransform.OffsetY *= MatrixTransform.Matrix.M22;

                // animate matrix change
                var matrixAnimation = new MatrixAnimation(MatrixTransform.Matrix, matrixTransform, new Duration(new TimeSpan(0, 0, 0, 0, 200)));
                matrixAnimation.Completed += (sender, ev) => { 
                    MatrixTransform.Matrix = matrixTransform; 
                    MatrixTransform.BeginAnimation(MatrixTransform.MatrixProperty, null);

                    Position = SurfaceMatrixTransform.Transform(Position);
                };
                MatrixTransform.BeginAnimation(MatrixTransform.MatrixProperty, matrixAnimation);

                SetValue(Canvas.ZIndexProperty, IsSelected ? (int)(PadLayers.SelectedPinnedPadLayer) : (int)(PadLayers.UnselectedPinnedPadLayer));
            }
            else
            {
                var matrixTransform = MatrixTransform.Matrix;
                matrixTransform.Scale(SurfaceMatrixTransform.M11, SurfaceMatrixTransform.M22);
                matrixTransform.OffsetX /= SurfaceMatrixTransform.M11;
                matrixTransform.OffsetY /= SurfaceMatrixTransform.M22;

                var matrixAnimation = new MatrixAnimation(MatrixTransform.Matrix, matrixTransform, new Duration(new TimeSpan(0, 0, 0, 0, 200)));
                matrixAnimation.Completed += (sender, ev) => { 
                    MatrixTransform.Matrix = matrixTransform; 
                    MatrixTransform.BeginAnimation(MatrixTransform.MatrixProperty, null);

                    var inverseTransform = SurfaceMatrixTransform;
                    inverseTransform.Invert();
                    Position = inverseTransform.Transform(Position);
                };
                MatrixTransform.BeginAnimation(MatrixTransform.MatrixProperty, matrixAnimation);

                SetValue(Canvas.ZIndexProperty, IsSelected ? (int)(PadLayers.SelectedPadLayer) : (int)(PadLayers.UnselectedPadLayer));
            }

            if (Container.HasDynamicLayout)
            {
                Container.ArrangePads();
            }
        }

        /// <summary>
        /// Invoked when an unhandled <see cref="E:System.Windows.Input.Mouse.PreviewMouseDown" /> attached routed event reaches an element in its route that is derived from this class. Implement this method to add class handling for this event.
        /// </summary>
        /// <param name="e">The <see cref="T:System.Windows.Input.MouseButtonEventArgs" /> that contains the event data. The event data reports that one or more mouse buttons were pressed.</param>
        protected override void OnPreviewMouseDown(MouseButtonEventArgs e)
        {
            base.OnPreviewMouseDown(e);
            IsSelected = true;
        }

        /// <summary>
        /// Called when the <see cref="T:System.Windows.Controls.ListBoxItem" /> is selected in a <see cref="T:System.Windows.Controls.ListBox" />.
        /// </summary>
        /// <param name="e">The event data.</param>
        protected override void OnSelected(RoutedEventArgs e)
        {
            base.OnSelected(e);
            SetValue(Canvas.ZIndexProperty, IsPinned ? (int)(PadLayers.SelectedPinnedPadLayer) : (int)(PadLayers.SelectedPadLayer));
        }

        /// <summary>
        /// Called when the <see cref="T:System.Windows.Controls.ListBoxItem" /> is unselected in a <see cref="T:System.Windows.Controls.ListBox" />.
        /// </summary>
        /// <param name="e">The event data.</param>
        protected override void OnUnselected(RoutedEventArgs e)
        {
            base.OnUnselected(e);
            SetValue(Canvas.ZIndexProperty, IsPinned ? (int)(PadLayers.UnselectedPinnedPadLayer) : (int)(PadLayers.UnselectedPadLayer));
        }

        /// <summary>
        /// Handles the Loaded event of the Pad control.
        /// </summary>
        /// <param name="sender">The source of the event.</param>
        /// <param name="e">The <see cref="RoutedEventArgs"/> instance containing the event data.</param>
        private static void OnLoaded(object sender, RoutedEventArgs e)
        {
            var pad = sender as Pad;
            if (pad == null)
            {
                throw new InvalidCastException();
            }
            var container = pad.SafeFindAncestor<Visual>(v => v is IPadContainer) as IPadContainer;
            if (container == null)
            {
                throw new InvalidOperationException("A pad must br attached to an IPadContainer");
            }
            pad.container = container;
        }

        /// <summary>
        /// Called when the Pad position within the surface has changed.
        /// </summary>
        /// <param name="sender">The sender.</param>
        /// <param name="e">The <see cref="DependencyPropertyChangedEventArgs"/> instance containing the event data.</param>
        /// <exception cref="System.InvalidOperationException"></exception>
        private static void OnPositionChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
        {
            var pad = sender as Pad;
            if (pad == null)
            {
                throw new InvalidOperationException();
            }

            if (!pad.isPinned)
            {
                pad.UpdateRenderTransform();
            }
        }

        private void UpdateRenderTransform()
        {
            // first apply pad position translation
            var transformationMatrix = new Matrix();
            transformationMatrix.Translate(Position.X, Position.Y);

            // then append surface matrix transformation
            transformationMatrix.Append(SurfaceMatrixTransform);

            MatrixTransform.Matrix = transformationMatrix;
        }

        /// <summary>
        /// Closes this Pad instance and removes it from its parent Surface pads Collection.
        /// </summary>
        public virtual void Close()
        {
            var closeMatrix = MatrixTransform.Matrix;
            closeMatrix.ScaleAt(0, 0, MatrixTransform.Matrix.OffsetX + ActualWidth / 2, MatrixTransform.Matrix.OffsetY + ActualHeight / 2);
            var closeAnimation = new MatrixAnimation(MatrixTransform.Matrix, closeMatrix, new Duration(new TimeSpan(0, 0, 0, 0, 150)));
            closeAnimation.Completed += closeAnimation_Completed;
            RenderTransform.BeginAnimation(MatrixTransform.MatrixProperty, closeAnimation);
        }

        /// <summary>
        /// Handles the Completed event of the closeAnimation control.
        /// </summary>
        /// <param name="sender">The source of the event.</param>
        /// <param name="e">The <see cref="EventArgs"/> instance containing the event data.</param>
        /// <exception cref="System.NotImplementedException"></exception>
        private void closeAnimation_Completed(object sender, EventArgs e)
        {
            Container.RemovePad(this);
        }

        /// <summary>
        /// Gets the container containing this pad.
        /// </summary>
        /// <value>
        /// The container.
        /// </value>
        internal IPadContainer Container
        {
            get
            {
                return container;
            }
        }

        /// <summary>
        /// Gets the matrix transform.
        /// </summary>
        /// <value>
        /// The matrix transform.
        /// </value>
        internal MatrixTransform MatrixTransform
        {
            get
            {
                if (matrixTransform == null)
                {
                    matrixTransform = new MatrixTransform();
                }
                return matrixTransform;
            }
        }


        /// <summary>
        /// Gets the default style.
        /// </summary>
        /// <value>
        /// The default style.
        /// </value>
        protected static Style DefaultStyle
        {
            get
            {
                if (defaultStyle == null)
                {
                    defaultStyle = Application.Current.TryFindResource("PadDefaultStyle") as Style;
                }
                return defaultStyle;
            }
        }

        /// <summary>
        /// Gets the default template.
        /// </summary>
        /// <value>
        /// The default template.
        /// </value>
        protected static ControlTemplate DefaultTemplate
        {
            get
            {
                if (defaultTemplate == null)
                {
                    defaultTemplate = Application.Current.TryFindResource("PadDefaultTemplate") as ControlTemplate;
                }
                return defaultTemplate;
            }
        }

        #region Private fields
        private IPadContainer container;
        private bool isPinned;

        private MatrixTransform matrixTransform;

        private static Style defaultStyle;
        private static ControlTemplate defaultTemplate;
        #endregion
    }
}
